﻿using System;
using System.Collections.Generic;
using System.Text;

using System.Net.Sockets;
using System.Net;
using rkShared.Net;
using System.Threading;
using rkShared.Logging;
using rkShared.BEncode;

namespace rkClient
{
    public delegate void PlayerConnectionEventHandler(ClientInfo player);


    public class GameClient : IDisposable
    {

        public const int MAX_RECONNECT_TRIAL_COUNT = 3;
        public const int RECONNECT_TIME_INTERVAL = 3000;    //wait 3 seconds, reconnect...

        private int m_retryCount = 0;

        private string m_playerName;
        private string m_serverIP;
        private int m_port;
        private int m_localPort;
        private List<MessageReader> m_readers;


        private Socket m_udpSocket;
        private Socket m_tcpSocket;

        ClientInfo m_clientInfo;

        public event MessageReceivedEventHandler ChatMessageReceived;
        public event MessageReceivedEventHandler GameMessageReceived;
        public event PlayerConnectionEventHandler NewPlayerConnected;
        public event PlayerConnectionEventHandler PlayerDisconnected;
        public event EventHandler ServerDisconnected;
        public event EventHandler ConnectionError;

        public GameClient(string playerName, string serverIP, int port, int localPort)
        {
            m_playerName = playerName;
            m_serverIP = serverIP;
            m_port = port;
            m_localPort = localPort;
            m_clientInfo = new ClientInfo() { ClientID = AppGlobals.ClientID, Port = m_localPort, ColorPreference = AppGlobals.Color, NickName = m_playerName };

            m_readers = new List<MessageReader>();

            this.Connect();
        }
        /// <summary>
        /// Connects the client to server if not already connected.
        /// </summary>
        public void Connect()
        {
            if (m_tcpSocket == null || !m_tcpSocket.Connected)
            {
                this.InitTcpSocket(m_serverIP, m_port, m_localPort);
                this.InitUdpSocket(m_localPort);
            }
        }
        /// <summary>
        /// Sends disconnect message. After calling this method, RegisterToServer must be called 
        /// to rejoin the game.
        /// </summary>
        public void Disconnect()
        {
            try
            {
                NetMessage msg = new NetMessage();
                msg.MessageType = MessageType.Disconnect;
                msg.Data = new BEncodedString("BYE BYE BEATIFUL");
                this.SendTCPMessage(msg);
            }
            catch
            {
            }
        }
        private void InitUdpSocket(int localPort)
        {
            m_udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            m_udpSocket.SetSocketOption(SocketOptionLevel.Socket,
                SocketOptionName.ExclusiveAddressUse,
                true);

            m_udpSocket.Bind(new IPEndPoint(IPAddress.Any, localPort));

            MessageReader _reader = new MessageReader(m_udpSocket);
            _reader.MessageReceived += new MessageReader.MessageReceivedEventHandler(this.NetMessageReceived);
            _reader.SocketErrorOccured += new EventHandler(UdpSocketErrorOccured);
            m_readers.Add(_reader);
            _reader.Start();
        }

        void UdpSocketErrorOccured(object sender, EventArgs e)
        {
            NetworkLogger.Log.Error("UDP ERROR OCCURED ON CLIENT SIDE");
        }
        private void InitTcpSocket(string serverIP, int port, int localPort)
        {
            m_tcpSocket = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream,
                ProtocolType.Tcp);

            m_tcpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true);
            m_tcpSocket.Bind(new IPEndPoint(IPAddress.Any, localPort));
            m_tcpSocket.Connect(new IPEndPoint(IPAddress.Parse(serverIP), port));
            
            MessageReader _reader = new MessageReader(m_tcpSocket);
            _reader.MessageReceived += new MessageReader.MessageReceivedEventHandler(NetMessageReceived);
            _reader.SocketErrorOccured += new EventHandler(TcpSocketErrorOccured);
            m_readers.Add(_reader);
            _reader.Start();
        }
        void TcpSocketErrorOccured(object sender, EventArgs e)
        {
            this.OnSocketError();
        }
        private void OnSocketError()
        {
            if (!this.Disposing)
            {
                this.Dispose();

                while (m_retryCount < MAX_RECONNECT_TRIAL_COUNT)
                {
                    m_retryCount++;
                    try
                    {
                        this.Connect();
                        m_retryCount = 0;
                    }
                    catch (Exception ex)
                    {
                        if (m_retryCount == MAX_RECONNECT_TRIAL_COUNT)
                        {
                            //GIVE UP...
                            NetworkLogger.Log.FatalFormat("COULD NOT RECONNECT TO SERVER AFTER TRYING {0} TIMES", MAX_RECONNECT_TRIAL_COUNT);
                            if (this.ConnectionError != null)
                                this.ConnectionError.BeginInvoke(this, EventArgs.Empty, this.EventFiredCallback, this.ConnectionError);
                        }
                        Thread.Sleep(RECONNECT_TIME_INTERVAL);
                        NetworkLogger.Log.ErrorFormat("COULD NOT RECONNECT, TRYING AGAIN");
                    }
                }
            }
        }
        protected void EventFiredCallback(IAsyncResult ar)
        {
        }
        void NetMessageReceived(MessageReader reader, NetMessage msg)
        {
            switch (msg.MessageType)
            {
                case MessageType.Chat:
                    this.OnChatMessageReceived(msg);
                    break;
                case MessageType.Register:
                    this.OnNewPlayer(msg);
                    break;
                default:
                    this.OnGameMessageReceived(msg);
                    //game messages..
                    break;
            }
        }

        private void OnGameMessageReceived(NetMessage msg)
        {
            if (msg.MessageType == MessageType.Disconnect)
            {
                if (msg.Data is ClientInfo)
                    this.PlayerDisconnected.BeginInvoke(msg.Data as ClientInfo, this.EventFiredCallback, null);
                else if (msg.Data is BEncodedString)
                {
                    if (this.ServerDisconnected != null)
                        this.ServerDisconnected.BeginInvoke(this, EventArgs.Empty, this.EventFiredCallback, null);
                }
            }
            else if (this.GameMessageReceived != null)
            {
                this.GameMessageReceived.BeginInvoke(null, msg, this.EventFiredCallback, null);
            }
        }
        private void OnNewPlayer(NetMessage msg)
        {
            ClientInfo ci = msg.Data as ClientInfo;
            if (ci != null && this.NewPlayerConnected != null)
                this.NewPlayerConnected.BeginInvoke(ci, this.EventFiredCallback, null);
        }
        private void OnChatMessageReceived(NetMessage msg)
        {
            if (this.ChatMessageReceived != null)
                this.ChatMessageReceived(null, msg);
        }
        public void RegisterToServer()
        {
            NetMessage _msg = new NetMessage();
            _msg.MessageType = MessageType.Register;
            _msg.Data = m_clientInfo;

            this.SendMessageToServer(_msg, m_tcpSocket, ProtocolType.Tcp);
        }
        public void SendTCPMessage(NetMessage msg)
        {
            this.SendMessageToServer(msg, m_tcpSocket, ProtocolType.Tcp);
        }
        public void SendUDPMessage(NetMessage msg)
        {
            this.SendMessageToServer(msg, m_udpSocket, ProtocolType.Udp);
        }
        private void SendMessageToServer(NetMessage msg, Socket socket, ProtocolType type)
        {
            msg.ClientID = AppGlobals.ClientID;
            ThreadPool.QueueUserWorkItem(this.SendMessageAsync, new object[] { msg, socket, type });
        }
        private void SendMessageAsync(object state)
        {
            try
            {
                object[] _parameters = state as object[];
                NetMessage _msg = _parameters[0] as NetMessage;
                Socket _sock = _parameters[1] as Socket;
                ProtocolType _type = (ProtocolType)_parameters[2];

                if (_type == ProtocolType.Udp)
                    _msg.SendOverUDP(_sock, m_serverIP, m_port);
                else
                    _msg.SendOverTCP(_sock);
            }
            catch (SocketException ex)
            {
                this.OnSocketError();
            }

        }

        #region IDisposable Members
        public void Dispose()
        {
            this.Dispose(5);
        }
        public void Dispose(int timeOut)
        {
            try
            {

                this.Disposing = true;
                this.Disconnect();
                foreach (MessageReader reader in m_readers)
                {
                    reader.Stop();
                }
                m_readers.Clear();
                m_tcpSocket.Close(timeOut);
                m_udpSocket.Close(timeOut);
            }
            catch
            {
            }
        }

        #endregion

        public bool Disposing { get; set; }
    }
}
